home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Nebula 1
/
Nebula One.iso
/
Financial
/
Stopwatch2.3
/
Source
/
Controller.m
< prev
next >
Wrap
Text File
|
1995-06-12
|
16KB
|
763 lines
/*
* Main controller for Stopwatch app.
*
* For legal stuff see the file COPYRIGHT
*/
#import <stdio.h>
#import <ansi/string.h> /* for import feature only (?) */
#import <bsd/sys/param.h> /* for MAXPATHLEN */
#import <appkit/NXCType.h>
#import "Controller.h"
#import "StopWatch.h"
#import "InfoPanel.h"
#import "ClientInfo.h"
#import "ClientInspector.h"
#import "SessionEditor.h"
#import "AppIconView.h"
#import "createPath.h"
#import "Preferences.h"
#define PRIORITY NX_MODALRESPTHRESHOLD
#define ARCHIVE_FILE "client.data"
#define TEMPLATE_DIR "Templates"
#define MAXCLIENTLEN 80
#define VERSION 2 /* the current file version that gets written */
int FileVersion; /* the version of the file being read */
void
freeAndCopy( char **ptr, const char *str )
{
if ( *ptr )
free( *ptr );
*ptr = NXCopyStringBuffer(str);
}
const char *
currentDate()
{
time_t now;
struct tm *tm;
static char buf[10];
time(&now);
tm = localtime(&now);
sprintf( buf, "%02d/%02d/%02d", tm->tm_mon + 1, tm->tm_mday, tm->tm_year );
return buf;
}
const char *
currentTime()
{
time_t now;
struct tm *tm;
static char buf[10];
time(&now);
tm = localtime(&now);
sprintf( buf, "%02d:%02d", tm->tm_hour, tm->tm_min );
return buf;
}
/*
* To avoid having to exec /bin/cp.
*/
int
copyFile( const char *src, const char *dst )
{
FILE *in, *out;
int count;
char buf[BUFSIZ];
if ( ! (in = fopen( src, "r" ) ) ) {
fprintf( stderr, "Can't open `%s' for reading.\n", src );
return 0;
}
if ( ! (out = fopen( dst, "w" ) ) ) {
fprintf( stderr, "Can't open `%s' for writing.\n", dst );
fclose(in);
return 0;
}
while ( (count = fread( buf, sizeof(char), sizeof(buf), in )) > 0 )
fwrite( buf, sizeof(char), count, out );
fclose(in);
fclose(out);
return 1;
}
@interface Controller(PRIVATE)
- selectedClient;
- (int)compare:obj1 :obj2; /* comparison method for SortList */
- initInvoice;
- (void)addSession:(const char *)startDate
time:(const char *)startTime
duration:(int)minutes
description:(const char *)desc;
- (void)checkStartButton;
@end
@implementation Controller
DPSTimedEntryProc
showElapsedTime(DPSTimedEntry teNum, double now, char *data)
{
[(id)data showElapsedTime];
return (void *)NULL;
}
- (void) removeTimedEntry
{
if ( teNum ) {
DPSRemoveTimedEntry(teNum);
teNum = 0 ;
}
}
- (void) addTimedEntry
{
[self removeTimedEntry]; /* in case there is one */
/* Set it up so that the clock updates every minute */
teNum = DPSAddTimedEntry( (double)60.0, (DPSTimedEntryProc)showElapsedTime,
(void *)self, PRIORITY );
}
- free
{
[self removeTimedEntry];
[stopwatch free];
[infoPanel free];
return [super free];
}
- awakeFromNib
{
[window setFrameAutosaveName:"Stopwatch"];
/* Make the browser's font match the startButton (can't do this in IB) */
[[[browser matrixInColumn:0] prototype] setFont:[startButton font]];
return self;
}
- add:sender
{
[[ClientInspector sharedInstance] add:sender];
return self;
}
- modify:sender
{
[[ClientInspector sharedInstance] modify:sender];
return self;
}
- delete:sender
{
[[ClientInspector sharedInstance] delete:sender];
return self;
}
- undelete:sender
{
[[ClientInspector sharedInstance] undelete:sender];
return self;
}
- (void)enableAdd:(BOOL)flag
{
[addMenuItem setEnabled:flag];
}
- (void)enableModify:(BOOL)flag
{
[modifyMenuItem setEnabled:flag];
}
- (void)enableDelete:(BOOL)flag
{
[deleteButton setEnabled:flag];
}
- (void)enableUndelete:(BOOL)flag
{
[undeleteButton setEnabled:flag];
}
/*
* Redisplay from the data in the clientList. Try to re-select the
* same item afterwards.
*/
- (void)decacheBrowser
{
/*
* Find the possibly new position in the list of the selected client,
* BEFORE redisplaying the browser from the list...
*/
int row = [clientList indexOf:[self selectedClient]];
[browser loadColumnZero];
[[browser matrixInColumn:0] selectCellAt:row :0];
[self checkStartButton];
}
- (NXTypedStream *)openArchive:(int)mode
{
NXTypedStream *stream ;
if ( (stream = NXOpenTypedStreamForFile( filename, mode )) == NULL ) {
NXRunAlertPanel( [NXApp appName], "Unable to open client data file: `%s'",
"Create it when needed", NULL, NULL, filename );
return nil;
}
return stream;
}
/*
* Read in the client info from the typestream file
*/
- (int)loadClientInfo
{
NXTypedStream *stream ;
if ( (stream = [self openArchive:NX_READONLY]) == nil )
return 0;
NXReadType( stream, "i", &FileVersion );
[clientList read:stream];
[clientList sort];
NXCloseTypedStream(stream) ;
return 1;
}
- (int)saveClientInfoToStream:(NXTypedStream *)stream
{
int version = VERSION;
NXWriteType( stream, "i", &version );
[clientList write:stream];
return 1;
}
- (int)saveClientInfo
{
NXTypedStream *stream ;
char backup[FILENAME_MAX + 1];
/*
* If this is the first write, move the old filename to
* filename~ to serve as a backup.
*/
if ( didBackup == NO ) {
sprintf( backup, "%s~", filename );
rename( filename, backup );
didBackup = YES;
}
if ( (stream = [self openArchive:NX_WRITEONLY]) == nil )
return 0;
[self saveClientInfoToStream:stream];
NXCloseTypedStream(stream);
return 1;
}
/*
* Edit the selected invoicing template by messaging to the
* workspace to open the corresponding file. Sender is the
* Matrix containing the menu of template names.
*/
- editTemplate:sender
{
id cell = [sender cellAt:[sender selectedRow] :0];
[self initInvoice];
[invoice editTemplate:[cell title]];
return self;
}
- preferences:sender
{
[preferences display];
return self;
}
- saveAs:sender
{
SavePanel *savePanel = [SavePanel new];
NXTypedStream *stream;
const char *path;
if ( [savePanel runModalForDirectory:dirname file:""] == 0 )
return nil;
path = [savePanel filename];
if ( (stream = NXOpenTypedStreamForFile( path, NX_WRITEONLY )) == NULL ) {
NXRunAlertPanel( [NXApp appName], "Unable to open file for writing: `%s'",
"What the...?", NULL, NULL, path );
return nil;
}
[self saveClientInfoToStream:stream];
NXCloseTypedStream(stream);
return self;
}
- clientList
{
return clientList;
}
- appDidInit:sender
{
NXRect rect = {{0.0, 0.0}, {64.0, 64.0}};
if ( createPath( dirname, DIRMODE ) != PathCreationOk ) {
NXRunAlertPanel( [NXApp appName], "Cannot create path `%s'",
"Damned UNIX!", NULL, NULL, dirname );
[NXApp terminate:sender];
}
preferences = [Preferences new];
[self loadClientInfo];
[self decacheBrowser];
if ( [preferences hideOnAutoLaunch] )
[NXApp hide:self];
else
[window makeKeyAndOrderFront:self];
/* make view that tracks elapsedTime be the appIcon window's contentView */
appIconView = [[AppIconView alloc] initFrame:&rect
sourceView:elapsedTimeField];
[[[NXApp appIcon] setContentView:appIconView] free];
[browser setDoubleAction:@selector(inspect:)];
[browser setTarget:self];
/* If there are no clients defined yet, disable the start buttons */
[self checkStartButton];
return self;
}
/*
* If we logout, or there's a powerOff, make sure the time gets saved.
*/
- app:sender powerOffIn:(int)ms andSave:(int)aFlag
{
return [self appWillTerminate:sender];
}
- appDidUnhide:sender
{
[window makeKeyAndOrderFront:self];
return self;
}
- appWillTerminate:sender
{
if ( teNum )
[self stopClock];
return self;
}
- init
{
char path[FILENAME_MAX + 1];
[super init];
stopwatch = [[StopWatch alloc] init];
clientList = [[SortList alloc] init];
[clientList setAutoSort:YES];
[clientList setDelegate:self];
sprintf( path, "%s/Library/%s", NXHomeDirectory(), [NXApp appName] );
dirname = NXCopyStringBuffer(path);
sprintf( path, "%s/%s", dirname, ARCHIVE_FILE );
filename = NXCopyStringBuffer(path);
return self;
}
- (const char *) description
{
return [description stringValue];
}
/*
* Called once per minute by the timed entry routine while the clock is running.
*/
- showElapsedTime
{
[elapsedTimeField setStringValue:[stopwatch elapsedTime]];
[appIconView display];
return self;
}
/*
* Respond to the user's selection of a client
*/
- selectClient:sender
{
/* Assume that this means we should stop the previous client */
if ( [stopwatch running] == YES )
[startButton performClick:sender];
activeClient = [self selectedClient];
[description setStringValue:[activeClient lastDescription]];
[description selectText:sender];
return self;
}
/*
* The start button highlights, but we need to force the title to "Stop".
* Setting the Alternate Title didn't seem to do the right thing in IB.
*/
- startClock
{
id font = [elapsedTimeField font];
[elapsedTimeField setFont:[[FontManager new] convertWeight:YES of:font]];
[self addTimedEntry];
[startButton setTitle:"Stop"];
[startMenuItem setTitle:"Stop"];
[stopwatch startWatch];
[self showElapsedTime];
activeClient = [self selectedClient];
return self;
}
/*
* The mirror image of the above routine
*/
- stopClock
{
id font = [elapsedTimeField font];
[elapsedTimeField setFont:[[FontManager new] convertWeight:NO of:font]];
[self removeTimedEntry];
[startButton setTitle:"Start"];
[startMenuItem setTitle:"Start"];
[stopwatch stopWatch];
[self showElapsedTime];
[self addSession:[stopwatch startDateString]
time:[stopwatch startTimeString]
duration:[stopwatch elapsedMinutes]
description:[self description]];
activeClient = nil;
return self;
}
/*
* Called whenever the startButton is pressed.
*/
- buttonHandler:sender
{
if ( [startButton state] == 1 )
[self startClock];
else
[self stopClock];
return self;
}
- showInfo:sender
{
[[InfoPanel new] showInfo];
return self;
}
/*
* Inspect the currently selected client
*/
- inspect:sender
{
Matrix *matrix = [browser matrixInColumn:0];
ClientInspector *inspector = [ClientInspector sharedInstance];
[inspector selectClientAt:[matrix selectedRow]];
[inspector display];
return self;
}
- inspectSessions:sender
{
[[ClientInspector sharedInstance] showHours:sender];
return self;
}
- inspectExpenses:sender
{
[[ClientInspector sharedInstance] showExpenses:sender];
return self;
}
- inspectClients:sender
{
[[ClientInspector sharedInstance] showClient:sender];
return self;
}
- generateDetail:sender
{
[self initInvoice];
[invoice generate:clientList];
return self;
}
/*
* Find a client by short name
*/
- (ClientInfo *)findClient:(const char *)name
{
int i, count = [clientList count];
for ( i = 0; i < count; i++ ) {
ClientInfo *info;
info = [clientList objectAt:i];
if ( strcmp( name, [info shortName] ) == 0 )
return info ;
}
return nil;
}
/*
* Compact consecutive sessions with identical descriptions into
* a single session with the same total time.
*/
- compactClients:sender
{
int i, count = [clientList count];
for ( i = 0; i < count; i++ )
[[clientList objectAt:i] compactSessions];
[[ClientInspector sharedInstance] display];
[self saveClientInfo];
return self;
}
/*
* This needs to be cleaned up...
*/
- import:sender
{
FILE *fp;
const char *pathname;
char buf[512], *tok;
char shortName[80], startDate[80], startTime[80], minutes[80], desc[256];
id openPanel = [OpenPanel new];
ClientInspector *inspector = [ClientInspector sharedInstance];
char delimiter[2], endDelimiters[10];
if ( [openPanel runModal] == 0 )
return nil;
pathname = [openPanel filename];
if ( ! (fp = fopen( pathname, "r" ) ) ) {
NXRunAlertPanel( [NXApp appName], "Unable to open import file: `%s'",
"Eat me!", NULL, NULL, pathname );
return self;
}
sprintf( delimiter, "%c", DELIMITER );
sprintf( endDelimiters, "%c\n", DELIMITER );
while ( fgets(buf, sizeof(buf), fp) ) {
ClientInfo *info;
Session *session;
tok = strtok( buf, delimiter );
strcpy( shortName, tok ) ;
if ( ! (info = [self findClient:shortName]) ) {
NXRunAlertPanel( [NXApp appName], "Ignoring unknown client: `%s'",
"Who needs 'em?", NULL, NULL, shortName );
continue;
}
tok = strtok( NULL, delimiter );
strcpy( startDate, tok );
tok = strtok( NULL, delimiter );
strcpy( startTime, tok );
tok = strtok( NULL, delimiter );
strcpy( minutes, tok );
tok = strtok( NULL, endDelimiters ); /* throw out newline too. */
strcpy( desc, tok );
session = [[Session alloc]
init:startDate time:startTime
duration:atoi(minutes) description:desc];
[info addSession:session];
[inspector updatedInfo:info];
}
fclose(fp);
[self saveClientInfo];
return self;
}
- export:sender
{
FILE *fp;
const char *pathname;
int i, count = [clientList count];
id savePanel = [SavePanel new];
if ( [savePanel runModal] == 0 )
return nil;
pathname = [savePanel filename];
if ( ! (fp = fopen( pathname, "w" ) ) ) {
NXRunAlertPanel( [NXApp appName], "Unable to open export file: `%s'",
"I'll be darned!", NULL, NULL, pathname );
return self;
}
for ( i = 0; i < count; i++ )
[[clientList objectAt:i] exportToFile:fp];
fclose(fp);
return self;
}
/*
* Clear out all session information from all clients.
*/
- closeMonth:sender
{
ClientInspector *inspector = [ClientInspector sharedInstance];
/* Give the user a chance to change their mind... */
if ( NXRunAlertPanel( [NXApp appName], "Delete all session and expense data?",
"Delete all data", "Hell no!", NULL ) == NX_ALERTDEFAULT ) {
[clientList makeObjectsPerform:@selector(deleteSessionsAndExpenses)];
[inspector closeMonth];
[self saveClientInfo]; /* write the newly empty file */
[inspector display];
}
return self;
}
@implementation Controller(PRIVATE)
- initInvoice
{
char path[FILENAME_MAX + 1];
if ( invoice == nil ) {
sprintf( path, "%s/%s", dirname, TEMPLATE_DIR );
invoice = [[Invoice alloc] initTemplateDir:path];
}
return invoice;
}
- (int)selectedRow
{
return [[browser matrixInColumn:0] selectedRow];
}
- selectedClient
{
return [clientList objectAt:[self selectedRow]];
}
/*
* Make sure the start buttons are disabled if there are
* no clients defined, and enabled if there are.
*/
- (void)checkStartButton
{
BOOL flag = ( [clientList count] ? YES : NO );
[startMenuItem setEnabled:flag];
[startButton setEnabled:flag];
/* the same goes for these */
[sessionMenuItem setEnabled:flag];
[expenseMenuItem setEnabled:flag];
/*
* The only reasonable thing to do if there are no
* clients is to create some!
*/
if ( flag == NO )
[self inspectClients:nil];
}
/*
* Create a new session object and add it to the proper client's
* ClientInfo list. Tell the browser what happened so it can update.
*/
- (void)addSession:(const char *)startDate
time:(const char *)startTime
duration:(int)minutes
description:(const char *)desc
{
Session *session = [[Session alloc]
init:startDate time:startTime
duration:minutes description:desc];
[activeClient addSession:session];
[[ClientInspector sharedInstance] updatedInfo:activeClient];
[self saveClientInfo];
}
/*
* Compare two ClientInfo objects. Sort alpha by long name.
*/
- (int)compare:obj1 :obj2
{
return strcmp( [obj1 clientName], [obj2 clientName] );
}
/*
* Delegated method of NXBrowser. This should be consolidated into a single
* object. Right now this method appears (almost) identically in the Controller
* and the ClientMgr... (Here we use the shortName instead of the full one.)
*/
- (int) browser:sender fillMatrix:matrix inColumn:(int)column
{
int i, count = [clientList count];
for ( i = 0; i < count; i++ ) {
const char *name;
id cell;
[matrix addRow];
name = [[clientList objectAt:i] shortName];
cell = [matrix cellAt:i :0]; /* 1 dimen. matrix: always use col 0! */
[cell setStringValue:name];
[cell setLoaded:YES];
[cell setLeaf:YES];
}
return count ;
}
@end